RailsでAPIサーバを開発する(AngularJS, Ruby on Rails, SPA)
RailsでJSONを返すAPIアプリケーションを3週間ぐらい試行錯誤しながら作成しています。少しですがノウハウも溜まってきたのでここにまとめておこうと思います。
今回のアプリケーションの構成は大体次のようになっています。
- RailsはAPIサーバ(一般公開するAPIではなくSPA(シングルページアプリケーション)のサーバとしてJSONを返却する。HTMLは返却しない)
- クライアントサイドはAngularJSで画面遷移、Viewの描画まで管理する
- DBはMySql、Session管理はRedis(まだローカル開発なのであまり関係無い)
チームはサーバサイド、クライアントサイドで完全に分担して二人で作成しています(自分はサーバサイド担当)。
このブログエントリーでは次のことを書きます。
- APIのルーティングの設定(JSONのみ返すようにする方法)
- Session管理(CSRFトークンの受け渡し方法など)
- APIのテスト方法(RSpecを使っています)
APIのルーティング
JSON formatの指定
各Routingをnamespace :api {format: 'json'}を全体に囲みます フォーマットをjson固定で指定したので、URLに「.json」をつけなくてもJSONが返却されます。 (「/api/users」というURLでJSONが返却されます)
namespace :api, { format: 'json' } do resources :admin_users resources :domains do resources :sites end post 'login' => 'user_sessions#create' delete 'logout' =>'user_sessions#destroy' route to: 'welcome#index' end
Controllerのnamespace
routes.rbでapiというnamespaceで全体を囲んでいるのでControllerにもnamespaceを付けます。付けたnamespaceに合わせてディレクトリも作成します(app/controllers/apiディレクトリを作成)。
module Api class UsersController < ApplicationController def index @users = User.all render json: @users end end
Session(Cookie)の管理
CSRF-Tokenとsession_storeについて書きます。
CSRF-Token
今回のアプリケーションではサーバサイドでViewをレンダリングしないので、そのままではクライアントとのCSRF−Tokenの受け渡しができません。
なのでトークンの受け渡しをするためにクライアント(今回はAngularJS)側で、リクエストヘッダーのX−CSRF-Tokenにサーバから渡されたトークンを設定してリクエストを送ります。
とりあえず開発フェーズでそこを後回しにしたい場合はCSRF-Tokenを無効にすることもできます。
module Api class ApplicationController < ActionController::Base protect_from_forgery with: :exception skip_before_filter :verify_authenticity_token end end
(protect_from_forgery with: null_sessionだとcookie自体が無効になってしまうのでこのように指定しています )
session_store
純粋なAPIサーバの場合、アクセストークンを発行して有効期限を設定するというやり方が一般的です。
ですが今回はSPA用のサーバとしてRailsアプリケーションをAPIサーバにしているだけなので、通常のRailsアプリケーションと同様にCookieを利用してSession管理をしています(session_idの受け渡し方法として)。
話はそれますが今回作成しているアプリケーションはsession_soreとしてRedisを使用しているのでそこの設定を書いておきます。
- session_store.rbに書いてある元の設定は削除する
# Rails.application.config.session_store :cookie_store, key: '_spa-demo_session'
- 環境用設定ファイル(development.rb, production.rb)にredisの設定をする
config.session_store :redis_store, { key: '_spa-demo_session', servers: { host: 'localhost', port: '6379', db: '0' }, expire_after: 60.minutes }
APIのTest方法
spec/requestsを使ってテストします。今回のRailsアプリケーションは画面が無いためCapybaraは使いません。
コントローラのレスポンスをシュミレートするためにget、post、put、deleteメソッドを使います。
(以下はadmin_users_controller.rbというコントローラを作ったと仮定して、そのAPIに対するテストケースです)
一覧(index)API
describe 'GET admin_users API', type: :request do it 'populates an array of all admin_users and does not include delete_containers' do suzuki = create(:admin_user, user_name: "鈴木一朗", email: "suzuki@co.jp", container_nums: 5) yamada = create(:admin_user, user_name: "山田太郎", email: "yamada@co.jp", container_nums: 5) get "/api/admin/admin_users" expect(response.status).to eq 200 expect(assigns(:admin_users)).to match_array([@admin_user, suzuki, yamada]) end end
- レスポンスオブジェクトのステータスフィールドを確認します。
- match_arrayメソッドで返却値の配列に作成したオブジェクトが含まれていることを確認します。なおmatch_arrayは順番は保証しないので、順番の確認が必要な場合は結果のJSON配列を確認する必要があります。
詳細(show)API
describe 'GET admin_users#show API', type: :request do before :each do @admin_user = create(:admin_user, id: 1) login @admin_user end it 'assigns the requested admin_user to @admin_user' do get "/api/admin/admin_users/#{@admin_user.id}", nil expect(response).to have_http_status(:success) json = JSON.parse(response.body) expect(json["id"]).to eq @admin_user.id expect(json["user_name"]).to eq @admin_user.user_name expect(json["admin_user_role_id"]).to eq @admin_user.admin_user_role_id expect(json["email"]).to eq @admin_user.email end end
- 詳細APIではオブジェクトのそれぞれのフィールドを確認しています。responseオブジェクトのbodyフィールドをjsonオブジェクトに変換してから確認します。
- beforeメソッドのなかで実施している「login @admin_user」はこのAPIを表示するための認証処理をパスするために行っています(APIテストには直接関係ない処理ですが後で説明します)。
更新(update)API
describe 'PUT admin_users#update API', type: :request do before :each do @admin_user = create(:admin_user) login @admin_user end context "valid attributes" do it "changes admin_user's attributes" do updated_date = DateTime.new(2015, 7, 7, 11, 12, 13) admin_user = create(:admin_user, id: 2, email: "before@co.jp", user_name: '変更前', admin_user_role_id: 1, updated_at: updated_date) new_attributes = { id: 2, email: 'after@co.jp', user_name: '変更後', admin_user_role_id: 2, updated_at: updated_date} put "/api/admin/admin_users/#{admin_user.id}", new_attributes admin_user.reload expect(response).to be_success expect(admin_user.email).to eq('after@co.jp') expect(admin_user.user_name).to eq('変更後') expect(admin_user.admin_user_role_id).to eq(2) end end end
- APIをコールする時はリクエストパラメータをハッシュで指定するので、ここでもnew_attributesというハッシュを作成してputメソッドの引数に渡しています。
- putメソッド実行後、オブジェクトをreloadメソッドで更新して各フィールドがAPI実行後に期待値に変わったか確認しています
登録(post)API
describe 'POST admin_users API', type: :request do it 'creates admin_user' do params = { email: "test@co.jp", user_name: "テスト管理者", password: "password", password_confirmation: "password"} # 登録が成功してDBのレコードが一件増えていることを確認する expect { post "/api/admin/admin_users", params }.to change(AdminUser, :count).by(1) expect(response.status).to eq 201 end end
ここでは登録APIが成功していることを「expect { XXX }.to change(YYY).by(1) 」の箇所で確認しています。
私は今回のプロジェクトでほぼ初めてRSpecを使ったのですが、この書き方は最初ビビりました。。が、慣れてくると見やすいですね、多分。
これはRSpecのchangeというマッチャで次のように使います。
expect{ XXX }.to change{ YYY }.from(AAA).to(BBB)
XXXの処理をするとYYYのオブジェクトがAAAの状態からBBBの状態に変わることを期待する
削除(delete)API
登録とは反対に削除されることを確認するだけなのでコードは省略します。 changeマッチャを使って一件レコードが減っていることを確認すればよいだけです!
expect { delete "/api/admin/admin_users", id }.to change(AdminUser, :count).by(-1)
認証チェックをパスしてテストする方法
先ほど詳細APIのところで少し触れた以下の記述ですが、これはAPIをテストするために認証処理をシミュレートをするための記述です。
認証処理にSorceryというgemを使用していますが、独自の認証処理でもあってシミュレートする仕組みは大体同じだと思います。
before :each do @admin_user = create(:admin_user) login @admin_user end
1.ログインをシミュレートするためのモジュールの作成
「rspec/support」ディレクトリに「authentication.rb」というファイルを作成して次のように処理を書きます。
module Authentication def login user, password = 'password' user.update_attribute :password, password post '/api/admin/login', { email: user.email, password: password } end end
- postメソッドの引数にはloginのパスを指定する
- emailではなくてユーザー名で認証している場合は{ user_name: user.name, password: password }にする
2.作成したモジュールを読み込ませる
rails_helperに今回作成したモジュールを追加してテスト実行時に利用できるようにします
RSpec.configure do |config| config.include Sorcery::TestHelpers::Rails::Integration, type: :request # 追加 config.include Authentication # 追加 end
ここまでの設定をすれば、APIのテスト時に認証処理をパスすることができるようになります。
現段階でのAngularJS & RailsのSPAアプリケーションについての感想
最後にまだまだ触ったばかりでしっかりと理解はできていないのですがRailsとAngularJSによるSPAについての雑感です。
メリット次のようなものがあると感じています。
- サーバサイド、クライアントサイドにエンジニアを用意してチームを組めるのであれば分担して開発ができる
- クライアント開発(JS、デザイン)の実力者がいればその人の能力をフルに活かせる
一方デメリットはこんな感じでしょうか。
- 単純な画面(ログイン、一覧画面等)までクライアントサイドで全て作るのはむしろ生産性が下がる(気がする)
- APIのインターフェースが変更になるとどちらも影響を受ける
まだまだ始めたばかりなのでもう少しこのスタイルの開発を噛み砕いてきたらまた感想などを書きたいと思います。